介绍
访问者(Visitor)模式:封装某些作用于某种数据结构中各元素的操作,它可以在不改变数据结构的前提下定义作用于这些元素的新的操作。
访问者模式是一种将数据操作与数据结构分离的设计模式,它是 23 种设计模式中最复杂的一个,但是它的使用频率并不高,正如《设计模式》的作者 GOF 对访问者模式的描述:大多数情况下,并不需要使用访问者模式,但是当你一旦需要使用它时,那你就是真的需要它了。
访问者模式的基本想法是,软件系统中拥有一个由许多对象构成的、比较稳定的对象结构,这些对象的类都拥有一个 accept 方法用来接受访问者对象的访问。访问者是一个接口,它拥有一个 visit 方法,这个方法对访问到的对象结构中不同类型的元素做出不同的处理。在对象结构的每一次访问过程中,我们遍历整个对象结构,对每一个元素都实施 accept 方法,在每一个元素的 accept 方法中会调用访问者的 visit 方法,从而使访问者得以处理对象结构的每一个元素,我们可以针对对象结构设计不同的访问者类来完成不同的操作,达到区别对待的效果。
优点
- 各角色职责分离,符合单一职责原则。
- 具有优秀的扩展性。
- 使得数据结构和作用于结构上的操作解耦,使得操作集合可以独立变化。
- 灵活性。
缺点
- 具体元素对访问者公布细节,违反了迪米特原则。
- 违反了依赖倒置原则,为了达到“区别对待”而依赖了具体类,没有依赖抽象。
使用场景
- 对象结构比较稳定,但经常需要在此对象结构上定义新的操作。
- 需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免这些操作“污染”这些对象的类,也不希望在增加新操作时修改这些类。
结构与实现
模式包含以下主要角色。
- Visitor(抽象访问者):接口或者抽象类,为每一个元素(Element)声明一个访问的方法。
- ConcreteVisitor(具体访问者):实现抽象访问者中的方法,即对每一个元素都有其具体的访问行为。
- Element(抽象元素):接口或者抽象类,定义一个accept方法,能够接受访问者(Visitor)的访问。
- ConcreteElementA、ConcreteElementB(具体元素):实现抽象元素中的accept方法,通常是调用访问者提供的访问该元素的方法。
- Object Structure(对象结构):是一个包含元素角色的容器,提供让访问者对象遍历容器中的所有元素的方法,通常由 List、Set、Map 等聚合类实现。
- Client(客户端类):即要使用访问者模式的地方。
其结构图如下图所示。
访问者模式的实现代码如下:
程序的运行结果如下:
示例
利用“访问者(Visitor)模式”模拟艺术公司与造币公司的功能。
分析:艺术公司利用“铜”可以设计出铜像,利用“纸”可以画出图画;造币公司利用“铜”可以印出铜币,利用“纸”可以印出纸币。对“铜”和“纸”这两种元素,两个公司的处理方法不同,所以该实例用访问者模式来实现比较适合。
首先,定义一个公司(Company)接口,它是抽象访问者,提供了两个根据纸(Paper)或铜(Cuprum)这两种元素创建作品的方法;再定义艺术公司(ArtCompany)类和造币公司(Mint)类,它们是具体访问者,实现了父接口的方法;然后,定义一个材料(Material)接口,它是抽象元素,提供了 accept(Company visitor)方法来接受访问者(Company)对象访问;再定义纸(Paper)类和铜(Cuprum)类,它们是具体元素类,实现了父接口中的方法;最后,定义一个材料集(SetMaterial)类,它是对象结构角色,拥有保存所有元素的容器 List,并提供让访问者对象遍历容器中的所有元素的 accept(Company visitor)方法。
ANDROID 源码中的实现
Android 的编译时注解是一种访问者模式。编译时注解的核心原理依赖 APT (Annotation Processing Tools)实现。
编译时注解解析的基本原理是,在某些代码元素上(如类型、函数、字段等)添加注解,在编译时编译器会检查 AbstractProcessor 的子类,并且调用该类型的 process 函数,然后将添加了注解的所有元素都传递到 process 函数中,使得开发人员可以在编译期进行相应的处理。
编写注解处理器的核心是 AnnotationProcessorFactory 和 AnnotationProcessor 两个接口,后者表示的是注解处理器,而前者则是为某些注解类型创建注解处理器的工厂。
对于编译器来说,代码中的元素结构是基本不变的,例如,组成代码的基本元素由包、类、函数、字段、类型参数、变量。JDK 中为这些元素定义了一个基类,也就是 Element 类,它有如下几个子类:
- PackageElement 包元素,包含了某个包下的信息,可以获取到包名等;
- TypeElement:类型元素,如某个字段属于某种类型;
- ExecutableElement:可执行元素,代表了函数类型的元素;
- VariableElement:变量元素;
- TypeParameterElement:类型参数元素。
因为注解可以指定作用在哪些元素上,因此,通过上述的抽象来对应这些元素,例如下面这个注解,指定的是只能作用于方法上面,并且这个注解只能保留在 class 文件中(编译时注解):
该注解因为只能作用于函数类型,因此,它对应的元素类型就是 ExecutableElement,当我们想通过 APT 处理这个注解时就可以获取目标对象上的 Test 注解,并将所有这些元素转换为 ExecutableElement 元素,以便获取到它们对应的信息。
我们看看元素基类的实现,完整的路径为 javax.lang.model.element.Element。
可以看到 Element 定义了一个代码元素的一些通用接口,其中很显眼的就是 accept 函数,这个函数接收一个 ElementVisitor 和类型为 P 的参数,ElementVisitor 就是访问者类型,而 P 则用于传递一些额外的参数给 visitor。这是一个典型的访问者模式。
ElementVisitor 定义如下:
当 Visitor 对元素结构进行访问时,就可以针对不同的类型进行不同的处理。例如 SimpleElementVisitor6 就是其中一个访问者,它基本上没做什么操作,直接返回了元素的默认值。
另一个提取元素类型的访问者是 ElementKindVisitor6:
ElementKindVisitor6 对于不同的类型进行不同的处理,提取各个元素的类型信息,例如,上述代码中对于 Type 类型的元素将分别进行处理,如类、枚举、接口、注解等。
首先,编译器将代码抽象成一个代码元素的树,然后再编译时对整棵树进行遍历访问,每个元素都有一个 accept 函数接受访问者的访问,每个访问者中都有对应的 visit 函数,例如,visitType 函数就是对类型元素的访问,在每个 visit 函数中对不同的类型进行不同的处理,这样就达到了差异处理的效果,同时将数据结构和数据操作分离,使得每个类型的职责单一,易于升级维护。JDK 还特意预留了 visitUnknown 接口来应对 Java 语言后续发展可能添加元素类型的问题,灵活地将访问者模式的缺点优化。